我將真相刻在鋼板上,其餘的皆不可信。
-- 布蘭登·山德森, 迷霧之子:昇華之井
我注意到窗外有個告示板,或是加油站,又或是同為兩者的東西
告示板上寫著大大的數字,三不五時就會有像是會移動的盒子靠到看板下方,接著會從盒子那邊,伸出兩條平行的光管聯結看板,接著可以發現盒子中,裝載了看板當時顯示的數字。盒子收回光管後就離開了。
偶爾,也會看到告示牌伸出光管,橫跨天空向著遠處的什麼東西伸去,而之後板子上的數字就會換成另一個數字。
我問小動物這些到底是發生了什麼事。
小動物說這叫綁定啊,然後一付想起什麼的表情,叫我試著用迴圈去加總個一到十試試。
想說這個做過幾百次了不要浪費時間吧,但我還是試了。
這什麼鬼東西。
accu = 0
for i <- 1..10 do
accu = accu + i
end
accu #=> 0
「我應該沒有寫錯什麼吧?這根本沒有動啊,為什麼 accu 最後還是 0
?」
語法上是可執行的沒錯,但是在這些城市裡,變數不是這樣用的。你只是太習慣比較大的都市裡那個等號是指派的規則了,但那並不是唯一可用的規則。
我們再回頭想想數學,在數學裡,一旦你說 x = 0
,那麼 x
在這個語境裡,就永遠是 0
了。仔細想一下,如果你把 =
想成「之後用左邊的符號代表右邊的值」, x = x + 1
會是一個有點矛盾的語句。你想說的其實是「要用 x
符號來代表之前的x
符號代表的值加上 1
。」
若是在那個叫 Erlang 的都市裡,當你寫 X = X + 1
時,它會直接跳出錯誤說「這個變數符號已經有講好的意義了,不能再改了。剛剛講好 X
是 0
,現在又說是 1
,你是騙子嗎?」 (其實沒有後半句,或至少它不會印出來給你看)
但在 Elixir 裡,為了使用方便,他們做出了一點妥協。但…只有一點點而己。在這裡,變數是綁定,而非指派的。
而這兩者的差異,就讓我來畫給你看吧:
在其它的城市裡,當我們寫了 x = 0
,是意味著把變數 x
指派成 0
。而這時在魔法運作的記憶區塊裡會新增一個區塊,接著將 x
這個變數連結到區塊,並在這個區塊內寫入 0
的值。
而當我們再寫下 x = 10
時,我們會找到這個區塊,並將裡面寫的值直接改成 10
。
而在 Elixir 之城裡,當我們寫了 x = 0
,一樣會在記憶區裡新增區塊並連結,而在區塊內寫入 0
的值,到此為止都是一樣的。
但若你重新綁定 x
為 10 的時候,術法會在記憶體區裡再新增一個區塊寫值並連結,而原先的區塊保持不變。
在 Elixir 裡,變數不是被指派了一個值,而是與某個值綁定,而它也可以改成綁定其它的變數。但原先的值一旦寫下就寫下了,會保持在那不變,我們稱這種行為叫 immutable,不可變動的。有了這個特性,我們可以來談一下某個在其它國度裡很令人困惑的東西了…
讓我們先定義一個外界變數,並且寫一個匿名函式,讓它向外引用變數。接著改變這個變數的值,在其它的國家裡,會發生什麼事呢?在 JS 莊園裡,會是這樣的:
let x = 10
function foo(y) { return x + y }
foo(1) //=> 11
x = 99
foo(1) //=> 100
由於在 foo
的定義裡,並沒有 x
這參數,所以在 JS 莊園裡的函式會向上找看有沒有可用的 x
的定義。這次它找到了,是 10
。
於是在第一次 foo(1)
呼叫時,我們可以得到 10 + 1
,也就是 11
。
但接著我們把 x
改成 99
,並重新呼叫一次,這時我們會拿到 99 + 1
的結果,也就是 100
。
這個情況,在 Ruby 公國裡也一樣:
# ruby 語法
x = 10
foo = lambda {|y| x + y}
foo.(1) #=> 11
x = 99
foo.(1) #=> 100
有時人們設計的魔法依賴這種方式運作,而有時,則會造成困擾。
(我想起之前想要在網頁裡用迴圈綁定事件,結果按按鈕都出現一樣的值的窘境)
因為用一樣的引數呼叫同一個函式兩次,卻出現了不一樣的結果。
那麼在 Elixir 之城裡,一樣的寫法,會發生什麼事呢?
x = 10
foo = fn y -> x + y end
foo.(1) #=> 11
x = 99
foo.(1) #=> 11
一樣我們定義了變數 x
與引用它的匿名函式 foo
。先呼叫一次可以拿到 11
。
但當我們重新綁定 x
時,由於只是改變了外面那個 x
的繫結目標,所以已定義的函式,依然是使用原先綁定的那個值。所以每次呼叫函式時,只要給了一樣的引數,那永遠會有一樣的回傳值。
這就是所謂 閉包 (closure) 的本質,只要函式引用了外界變數,就是一個閉包。然而在函數式的語境裡,這個函式會將其當下的值封存起來,之後不管原先那個外界變數如何改變,每次用一樣的引數呼叫這個函式,都會拿到相同的結果,而這個行為,比較是數學上認為函數該有的樣子。
就像你們世界裡的琥珀化石一樣,一旦樹脂包住昆蟲或蠍子或植物後,它就保持著封存那一瞬間的姿態了。
而在其它選擇了 mutable 語境的國度裡,想要模仿這個行為,該怎麼辦呢?在那些地方,常常會採用回傳函式的函式這個手法,來製作出有相同行為的函式:
let x = 10
let bar = function(x) {
return function(y) { return x + y }
}
let foo = bar(x)
foo(1); //=> 11
x = 99
foo(1) //=> 11
我們先定義 bar
這個會回傳函式的函式,接受一個參數,並回傳一個函式。而回傳的函式裡引用到的參數,是呼叫 bar
當下的值。
而當我們呼叫 bar
傳入 x
時,拿到的回傳的函式 foo
,就會永遠引用那個 10
的值了。
就算我們之後再來改變外界的 x
,也已經跟呼叫 bar
那時無關了。
這些,都是在設計一個國度時的取捨。當然沒有了「可以改變內容的變數」,在操作上就看似相當受限。因此在這些國度裡,遞迴是非常非常重要的,他可以做到其它的地方用共有的可改變變數所做的事。但在採用了這個設計後,我們交換到的,是更容易除錯與測試,更容易平行化,以及最重要的,更貼近數學上的概念。
「所以,在這些語言…嗯…國度裡,等號就是變數綁定嗎?」
還要再更棒一些。
[to be continue]